package com.eelye.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.support.AopUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

@Slf4j
public class ReflectionUtil {

    private static final Method[] EMPTY_METHOD_ARRAY = new Method[0];

    private static final Map<Class<?>, HashMap<String, Set<Method>>> methodsCache = new HashMap<>(256);

    static public <T> T Call(Object obj, String methodName, Object... params)
            throws NoSuchMethodException, Throwable {
        return Call(obj, methodName, null, params);
    }

    /**
     * 调用一个方法。主要用来调用私有方法。
     *
     * @param obj        要调用的方法的所属的对象。
     * @param methodName 方法的名称
     * @param paramTypes 方法类型数组，可以传null，当传入为空时，会根据后面的参数确定参数类型。
     *                   主要是针对方法中含有基础类型导致无法找到方法而设计，如果有基础类型，无法根据传入的参数确定参数的类型。
     * @param params     调用方法时传入的参数。
     * @throws NoSuchMethodException 找不到方法时抛出该异常。Throwable为要调用的方法本身抛出的异常。
     * 其实Throwable包含了NoSuchMethodException，保留是为了明确告诉调用者注意这个exception。
     * @return 方法返回的对象.
     */
    static public <T> T Call(Object obj, String methodName, Class[] paramTypes, Object... params)
            throws NoSuchMethodException, Throwable {
        Object ret = null;

        if (null == paramTypes) {
            paramTypes = new Class[params.length];
            for (int i = 0; i < params.length; ++i) {
                paramTypes[i] = params[i].getClass();
            }
        }

        Method targetMethod = findMethod(obj.getClass(), methodName, paramTypes);
        if (null != targetMethod) {
            targetMethod.setAccessible(true);

            Object target = getTarget(obj);
            try {
                ret = targetMethod.invoke(target, params);
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        } else {
            throw new NoSuchMethodException(methodName);
        }

        targetMethod.setAccessible(false);

        return (T) ret;
    }

    /**
     * 给一个对象的属性设置值。主要用来设置私有对象的值。
     * @param obj 对象
     * @param field 属性
     * @param value 值
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    static public void Set(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field fieldObj = ReflectionUtils.findField(obj.getClass(), field);
        if (null != fieldObj) {
            fieldObj.setAccessible(true);
            fieldObj.set(obj, value);
        } else {
            throw new NoSuchFieldException(field);
        }

        fieldObj.setAccessible(false);
    }

    /**
     * 获取代理的目标对象。
     *
     * @param proxy 代理对象
     * @return
     * @throws Exception
     */
    public static <T> T getTarget(Object proxy) {
        if (!AopUtils.isAopProxy(proxy)) {
            return (T) proxy;//不是代理对象
        }

        try {
            if (AopUtils.isJdkDynamicProxy(proxy)) {
                return (T) getJdkDynamicProxyTargetObject(proxy);
            } else { //cglib
                return (T) getCglibProxyTargetObject(proxy);
            }
        } catch (Exception e) {
            log.error(e.toString());
        }

        return null;
    }


    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
        h.setAccessible(true);
        Object dynamicAdvisedInterceptor = h.get(proxy);
        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
        advised.setAccessible(true);
        Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
        return target;
    }


    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
        h.setAccessible(true);
        AopProxy aopProxy = (AopProxy) h.get(proxy);
        Field advised = aopProxy.getClass().getDeclaredField("advised");
        advised.setAccessible(true);
        Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
        return target;
    }

    public static Method findMethod(Class<?> clazz, String name, Class<?>[] paramTypes) {
        Assert.notNull(clazz, "Class must not be null");
        Assert.notNull(name, "Method name must not be null");

        if (!methodsCache.containsKey(clazz)) {
            Set<Method> allMethodSet = getAllMethodSet(clazz);

            if (!allMethodSet.isEmpty()) {
                cacheMethodSet(clazz, allMethodSet);
            }
        }

        Method ret = findMethodFromCache(clazz, name, paramTypes);

        return ret;
    }

    private static void cacheMethodSet(Class<?> clazz, Set<Method> methodSet) {
        synchronized (methodsCache) {
            HashMap<String, Set<Method>> methodMap = methodsCache.get(clazz);
            if (null == methodMap) {
                methodMap = new HashMap<>();
            }

            for (Method method : methodSet) {
                Set<Method> nameSet = methodMap.get(method.getName());
                if (null == nameSet) {
                    nameSet = new HashSet<>();
                }
                nameSet.add(method);

                methodMap.put(method.getName(), nameSet);
            }

            methodsCache.put(clazz, methodMap);
        }
    }

    private static Set<Method> getAllMethodSet(Class<?> clazz) {
        Set<Method> ret = new HashSet<>();

        Class<?> searchType = clazz;
        while (searchType != null) {
            Method[] methods = (searchType.isInterface() ? searchType.getMethods() :
                    getDeclaredMethods(searchType));
            ret.addAll(Arrays.asList(methods));

            searchType = searchType.getSuperclass();
            if (searchType.equals(Object.class)) {
                searchType = null;
            }
        }

        return ret;
    }

    private static Method findMethodFromCache(Class<?> clazz, String name, @Nullable Class<?>... paramTypes) {
        Map<String, Set<Method>> methodMap = methodsCache.get(clazz);
        if (!methodMap.isEmpty()) {
            Set<Method> methodSet = methodMap.get(name);
            if (null == methodSet) {
                return null;
            }

            for (Method method : methodSet) {
                if (name.equals(method.getName()) && (paramTypes == null || hasSameParams(method, paramTypes))) {
                    return method;
                }
            }

            //如果精确找了一遍没找到，开始模糊查找
            for (Method method : methodSet) {
                if (name.equals(method.getName()) && (paramTypes == null ||
                        typeEqual(method.getParameterTypes(), paramTypes))) {
                    return method;
                }
            }

            return null;
        }

        return null;
    }

    private static boolean hasSameParams(Method method, Class<?>[] paramTypes) {
        return (paramTypes.length == method.getParameterCount() &&
                Arrays.equals(paramTypes, method.getParameterTypes()));
    }

    private static Method[] getDeclaredMethods(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");

        Method[] result = null;

        try {
            Method[] declaredMethods = clazz.getDeclaredMethods();
            List<Method> defaultMethods = findConcreteMethodsOnInterfaces(clazz);
            if (defaultMethods != null) {
                result = new Method[declaredMethods.length + defaultMethods.size()];
                System.arraycopy(declaredMethods, 0, result, 0, declaredMethods.length);
                int index = declaredMethods.length;
                for (Method defaultMethod : defaultMethods) {
                    result[index] = defaultMethod;
                    index++;
                }
            } else {
                result = declaredMethods.clone();
            }
        } catch (Throwable ex) {
            throw new IllegalStateException("Failed to introspect Class [" + clazz.getName() +
                    "] from ClassLoader [" + clazz.getClassLoader() + "]", ex);
        }

        return result;
    }

    private static List<Method> findConcreteMethodsOnInterfaces(Class<?> clazz) {
        List<Method> result = null;
        for (Class<?> ifc : clazz.getInterfaces()) {
            for (Method ifcMethod : ifc.getMethods()) {
                if (!Modifier.isAbstract(ifcMethod.getModifiers())) {
                    if (result == null) {
                        result = new ArrayList<>();
                    }
                    result.add(ifcMethod);
                }
            }
        }
        return result;
    }

    private static boolean typeEqual(Class<?>[] factTypeArr, Class<?>[] userTypeArr) {
        if (factTypeArr.length != userTypeArr.length) {
            return false;
        }

        for (int i = 0; i < factTypeArr.length; ++i) {
            if (!factTypeArr[i].isAssignableFrom(userTypeArr[i]) && !fuzzyEqual(factTypeArr[i], userTypeArr[i])) {
                return false;
            }
        }

        return true;
    }

    private static boolean fuzzyEqual(Class<?> factType, Class<?> userType) {
        return factType.equals(userType) ||
                (factType.equals(int.class) && userType.equals(Integer.class)) ||
                (factType.equals(long.class) && userType.equals(Long.class)) ||
                (factType.equals(short.class) && userType.equals(Short.class)) ||
                (factType.equals(boolean.class) && userType.equals(Boolean.class)) ||
                (factType.equals(char.class) && userType.equals(Character.class)) ||
                (factType.equals(byte.class) && userType.equals(Byte.class)) ||
                (factType.equals(float.class) && userType.equals(Float.class)) ||
                (factType.equals(double.class) && userType.equals(Double.class));
    }
}
